using System;
using System.Collections.Generic;
using GGEditor.API.Utility.Collections;

namespace GGEditor.API.Commands
{
	public class CommandManager<T> : ICommandManager<T>, IUndoManager
	{
		private static readonly int MaximumUndoStackSize = 10000;

		protected T Context { get; set; }

		public CommandManager(T context)
		{
			redoStack = new BoundedStack<ICommand<T>>(MaximumUndoStackSize);
			undoStack = new BoundedStack<ICommand<T>>(MaximumUndoStackSize);
			Context = context;
		}

		public void Do(ICommand<T> command)
		{
			if (currentOperation != null)
				throw new InvalidOperationException(
					"Cannot do a command while an operation is pending");

			ICommand<T> undoCommand = command.Do(Context);
			if (command.IsDestructive)
			{
				undoStack.Clear();
				redoStack.Clear();
			}
			else if (undoCommand != null)
				undoStack.Push(undoCommand);
		}

		public class CommandOperation : ICommandOperation<T>
		{
			private bool active;
			private readonly string description;
			private readonly CommandManager<T> manager;
			private readonly IList<ICommand<T>> undoStack;
			private readonly IList<ICommand<T>> redoStack;

			public CommandOperation(CommandManager<T> manager, string description)
			{
				this.active = true;
				this.description = description;
				this.manager = manager;
				undoStack = new List<ICommand<T>>();
				redoStack = new List<ICommand<T>>();
			}

			#region Implementation of ICommandOperation<T>

			public void Do(ICommand<T> command)
			{
				if (!active)
					throw new InvalidOperationException(
						"Operation already ended or canceled");

				ICommand<T> undoCommand = command.Do(manager.Context);

				if (command.IsDestructive)
					throw new InvalidOperationException(
						"Destructive commands not supported by operation");

				if (undoCommand == null)
					throw new InvalidOperationException(
						"Command did not provide undoable command during operation");

				redoStack.Add(command);
				undoStack.Insert(0, undoCommand);
			}

			public void Cancel()
			{
				active = false;
				foreach (ICommand<T> undoCommand in undoStack)
				{
					undoCommand.Do(manager.Context);
				}
				manager.EndOperation();
			}

			public void End()
			{
				active = false;
				manager.undoStack.Push(new Command<T>()
					{
						Description = description,
						Do = delegate(T conext)
						{
							foreach (ICommand<T> undoCommand in undoStack)
							{
								undoCommand.Do(manager.Context);
							}
						},
						Undo = delegate(T conext)
						{
							foreach (ICommand<T> redoCommand in redoStack)
							{
								redoCommand.Do(manager.Context);
							}
						}
					});
				manager.EndOperation();
			}

			#endregion
		}

		private readonly IStack<ICommand<T>> undoStack;
		private readonly IStack<ICommand<T>> redoStack;

		#region Implementation of ICommandManager<T>

		private CommandOperation currentOperation;

		public ICommandOperation<T> BeginOperation(string description)
		{
			if (currentOperation != null)
				throw new InvalidOperationException(
					"Cannot start new operation before ending old one.");

			return currentOperation = new CommandOperation(this, description);
		}

		protected void EndOperation()
		{
			if (currentOperation == null)
				throw new InvalidOperationException(
					"No current operation; cannot end.");

			currentOperation = null;
		}

		#endregion

		#region Implementation of IUndoManager

		public bool HasUndo
		{
			get { return !undoStack.Empty; }
		}

		public string UndoDescription
		{
			get
			{
				if (!HasUndo)
					return null;
				return undoStack.Peek().Description;
			}
		}

		public void Undo()
		{
			if (!HasUndo)
				return;
			ICommand<T> cmd = undoStack.Pop();
			redoStack.Push(cmd.Do(Context));
		}

		public bool HasRedo
		{
			get { return !redoStack.Empty; }
		}

		public string RedoDescription
		{
			get
			{
				if (!HasRedo)
					return null;
				return redoStack.Peek().Description;
			}
		}

		public void Redo()
		{
			if (!HasRedo)
				return;
			ICommand<T> cmd = redoStack.Pop();
			undoStack.Push(cmd.Do(Context));
		}

		#endregion
	}
}